'use client'; import { useState, useEffect, useCallback, useRef } from 'react'; import { fetchApi } from '@/lib/utils/client'; import { useStudioContext } from '@/app/studio/context'; import type { CrewMemberItem, CrewMemberListResponse, SearchResultItem, SearchMemberResponse } from '@/types/response/crew/member'; import { Button } from '@/components/ui/button'; import { Input } from '@/components/ui/input'; import { Label } from '@/components/ui/label'; import { Checkbox } from '@/components/ui/checkbox'; import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter } from '@/components/ui/dialog'; function RequiredLabel({ htmlFor, children }: { htmlFor: string; children: React.ReactNode }) { return ; } const EMPTY_MEMBER_FORM = { nickname: '', role: '', sortOrder: 0, isActive: true }; type Props = { crewID: number; }; export default function CrewMembersTab({ crewID }: Props) { const { channelID } = useStudioContext(); const [members, setMembers] = useState([]); const [loading, setLoading] = useState(true); // 초대 코드 const [inviteCode, setInviteCode] = useState(null); const [generatingCode, setGeneratingCode] = useState(false); // 회원 검색 const [searchQuery, setSearchQuery] = useState(''); const [searchResults, setSearchResults] = useState([]); const [showSearch, setShowSearch] = useState(false); const searchTimer = useRef|null>(null); // 크루원 편집 모달 const [editModal, setEditModal] = useState<{ open: boolean; member: CrewMemberItem|null }>({ open: false, member: null }); const [editForm, setEditForm] = useState(EMPTY_MEMBER_FORM); const [saving, setSaving] = useState(false); const fetchMembers = useCallback(async () => { try { const res = await fetchApi(`/api/studio/crew/member/list/${crewID}`); setMembers(res.data?.list ?? []); } catch { } finally { setLoading(false); } }, [crewID]); useEffect(() => { fetchMembers(); }, [fetchMembers]); // 회원 검색 (디바운스) useEffect(() => { if (searchTimer.current) { clearTimeout(searchTimer.current); } if (searchQuery.trim().length < 2) { setSearchResults([]); return; } searchTimer.current = setTimeout(async () => { try { const res = await fetchApi( `/api/studio/crew/member/search?channelID=${channelID}&q=${encodeURIComponent(searchQuery)}&crewID=${crewID}` ); setSearchResults(res.data?.list ?? []); } catch {} }, 300); return () => { if (searchTimer.current) { clearTimeout(searchTimer.current); } }; }, [searchQuery, channelID, crewID]); const handleAddMember = async (item: SearchResultItem) => { try { await fetchApi('/api/studio/crew/member/add', { method: 'POST', body: { crewID, targetMemberID: item.memberID, nickname: item.name ?? item.email, sortOrder: members.length } }); setSearchQuery(''); setSearchResults([]); setShowSearch(false); fetchMembers(); } catch (err: unknown) { alert(err instanceof Error ? err.message : '추가에 실패했습니다.'); } }; const handleRemove = async (memberID: number) => { if (!confirm('이 크루원을 삭제하시겠습니까?')) { return; } try { await fetchApi(`/api/studio/crew/member/${memberID}`, { method: 'DELETE' }); fetchMembers(); } catch (err: unknown) { alert(err instanceof Error ? err.message : '삭제에 실패했습니다.'); } }; const openEdit = (member: CrewMemberItem) => { setEditForm({ nickname: member.nickname, role: member.role ?? '', sortOrder: member.sortOrder, isActive: member.isActive }); setEditModal({ open: true, member }); }; const handleUpdate = async () => { if (!editModal.member) { return; } setSaving(true); try { await fetchApi(`/api/studio/crew/member/${editModal.member.id}`, { method: 'PUT', body: { crewMemberID: editModal.member.id, nickname: editForm.nickname, role: editForm.role || null, sortOrder: editForm.sortOrder, isActive: editForm.isActive } }); setEditModal({ open: false, member: null }); fetchMembers(); } catch (err: unknown) { alert(err instanceof Error ? err.message : '수정에 실패했습니다.'); } finally { setSaving(false); } }; const handleGenerateCode = async () => { setGeneratingCode(true); try { const res = await fetchApi<{ inviteCode: string }>('/api/studio/crew/invite/generate', { method: 'POST', body: { crewID } }); setInviteCode(res.data?.inviteCode ?? null); } catch (err: unknown) { alert(err instanceof Error ? err.message : '코드 생성에 실패했습니다.'); } finally { setGeneratingCode(false); } }; const copyCode = () => { if (inviteCode) { navigator.clipboard.writeText(inviteCode); alert('복사되었습니다.'); } }; if (loading) return

준비 중...

; return ( <> {/* 초대 코드 */}
초대 코드
{inviteCode ? ( <> ) : ( )}
{/* 크루원 관리 */}

크루원 ({members.length}명)

{showSearch && (
setSearchQuery(e.target.value)} /> {searchResults.length > 0 && (
{searchResults.map(item => ( ))}
)}
)}
{members.length === 0 ? ( ) : members.map(m => ( ))}
순서닉네임역할채널상태가입일작업
등록된 크루원이 없습니다.
{m.sortOrder} {m.thumb && }{m.nickname} {m.role ?? '-'} {m.channelName ?? '-'} {m.isActive ? '활성' : '비활성'} {new Date(m.joinedAt).toLocaleDateString('ko-KR')}
{/* 크루원 편집 모달 */} { if (!open) setEditModal({ open: false, member: null }); }}> 크루원 수정
닉네임 setEditForm(f => ({ ...f, nickname: e.target.value }))} />
setEditForm(f => ({ ...f, role: e.target.value }))} placeholder="예: 게이머, MC" />
setEditForm(f => ({ ...f, sortOrder: Number(e.target.value) }))} />
setEditForm(f => ({ ...f, isActive: !!v }))} />
); }